feat(view): allow finalize bodies to call view functions#3253
Conversation
a3f40b7 to
cf6b381
Compare
Adds Eth-`view`-style invocation: a function's `finalize` body can call a view function (same-program or imported cross-program). Views themselves remain leaves — they still reject `is_call` at construction, so no recursion. Changes: - Cache view `FinalizeTypes` on `Stack` (previously recomputed per call). - Loosen `Finalize::add_command` to permit `call`; keep `call.dynamic` rejected. New `Command::is_dynamic_call` helper. - Type-check (`check_instruction_opcode`) resolves the target and allows `Opcode::Call` only when it lands on a view. `Call::output_types` gains a view branch alongside closure / function. - Runtime dispatch: `finalize_command_except_await` special-cases `Command::Instruction(Instruction::Call(_))` and defers to a new `evaluate_call_to_view` helper that loads inputs from the caller's registers, runs the view body against the live store with the caller's inherited `FinalizeGlobalState`, and writes outputs to the caller's destination registers. - Cost rollup: `cost_per_command` now folds the called view's `view_cost_for_single_view` into the caller's finalize cost, so the per-function `TRANSACTION_SPEND_LIMIT` check covers callee compute too. - V15 syntax gate: `Program::contains_v15_syntax` flags any `call` inside a finalize body (pre-V15 finalize forbade `call` entirely). - The view module is now unconditionally compiled (the in-block call path doesn't need the `history` feature); only the `evaluate_view_at_height` public API and `HistoricFinalizeStore` remain gated.
…output, struct return)
cf6b381 to
1e6645e
Compare
Drop the verbose explanation on the unconditional 'view' module declaration and correct the rationale for forbidding 'call.dynamic' in finalize: the real blocker is that we have not yet designed dynamic spend / gas tracking for runtime-resolved targets.
Restore Call::output_types to its original transition-only behavior and introduce Call::output_types_for_view for the finalize-side view-call type-check. Finalize-types initialize dispatches Instruction::Call to the new helper, making the API surface self-document the split and removing the risk of a future transition-path caller accidentally accepting a view target through the generic output_types entry.
view functions
Bail explicitly on `CallDynamic` in the finalize type-checker, add a per-operand input-type check in `Call::output_types_for_view`, and promote the post-view `finalize_operations.is_empty()` invariant to a release-time `ensure!`. Tests cover destination-count and input-type mismatches at deploy, runtime view failure, and branch interactions.
Relax `many1` → `many0` on view outputs in the parser, `FromBytes`/`ToBytes`, and `Program::add_view`. Enables Solidity-style cross-program precondition guards from finalize; rides the existing V15 gate.
22250b3 to
0afb918
Compare
Relax `many1` → `many0` on view commands in the parser, `FromBytes`/`ToBytes`, and `Program::add_view`. Brings view arity in line with `function` (all-`many0`) and removes an arbitrary restriction; no use case in mind, just consistency. Rides the existing V15 gate.
Replace the special case in `finalize_command_except_await` with a real `Call::finalize` that loads inputs from the caller's `FinalizeRegisters`, dispatches the view body through a new `StackTrait::evaluate_view` hook (implemented on `Stack` as a thin wrapper around `evaluate_view_inner`), and writes outputs back. The transition-side `evaluate_call_to_view` / `run_view_call` helpers in `process::view` are no longer needed and are removed. To plumb the finalize store into `Call::finalize`, `Instruction::finalize` gains a `store: &impl FinalizeStoreTrait<N>` parameter and tightens the registers bound from `RegistersTrait<N>` to `FinalizeRegistersState<N>` (the latter implies the former). Each per-instruction `finalize` impl ignores the new `_store` argument; only `Call::finalize` uses it. The instruction-level tests in `synthesizer/program/tests/instruction/` pass a new `NoopFinalizeStore` shim — a `bail!`-only `FinalizeStoreTrait` fixture — to satisfy the signature without pulling `snarkvm-ledger-store` into the program crate's dev-deps for fixtures that never touch the store.
…finalize Lets tests pass `None` instead of a dummy store fixture. `Call::finalize` errors when the store is missing.
vicsn
left a comment
There was a problem hiding this comment.
We need to update check_upgrade_is_valid. It's a pity that logic is so hidden and not tightly integrated.
We currently prevent a function from defining more than MAX_TRANSITIONS static calls in get_minimum_number_of_calls. Will we run into DoS trouble if someone defines a finalize scope calling into MAX_COMMANDS views? Defining a MAX_CALLS = MAX_TRANSITIONS is probably acceptable UX.
We could also consider changing check_program_is_well_formed to allow a deployment of programs with only views, not sure if this will break existing assumptions in the code. For example, deployments could then have 0 verifying keys. I'm leaning towards leaving this as future work.
Adds `MAX_CALLS = 32` (mirrors `MAX_TRANSITIONS`) enforced in `Finalize::add_command`, and extends `check_upgrade_is_valid` to require each old view's input/output types in the new program.
|
Re: vicsn's review
Addressed in fae8a28: Filed #3271 to track the view-only deployment relaxation as future work. |
Closes #3252.
A function's
finalizebody can nowcalla view function — same-program or imported. Views remain leaves (cannot call other views).Views may also declare zero outputs and serve as cross-program preconditions (Aleo analogue of Solidity's
function require_member(address) external view { require(...); }):What's enforced
Finalize::add_commandpermitsCall;call.dynamicrejected viaCommand::is_dynamic_call. Constructors still forbidcallentirely. View inputs/commands/outputs are allmany0(matchesfunction); body constraints (no record-touching ops, no state writes, noasync/await/call/rand.chacha) are enforced byViewCore::add_command.Opcode::Call(_)in a finalize body is allowed only when the target resolves to a view; each operand's type is checked against the view's declared input type, and the destination count must match the view's output count.call.dynamicis bailed in the finalize type-checker.Call::finalizeloads inputs from the caller'sFinalizeRegisters, dispatches the view body via a newStackTrait::evaluate_viewhook against the live finalize store with the caller'sFinalizeGlobalState, and writes outputs back. A view-body failure surfaces as a finalize rejection.cost_per_commandfolds the callee'sview_cost_for_single_viewinto the caller's finalize cost, keepingTRANSACTION_SPEND_LIMITas the combined bound.Program::contains_v15_syntaxflags anycallin a finalize body; arity relaxations ride the same gate.